package cn.bearspark.framework.redis.starter.buffer.zset;

import cn.bearspark.framework.redis.starter.buffer.RedisBuffer;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;

import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.IntSupplier;
import java.util.function.Supplier;

/**
 * Redis 的 ZSet 数据结构的缓存器的抽象父类
 * <p>
 * 该缓存器可以用作判断 long 类型的数据是否存在 (过滤出存在的数据)
 *
 * @author f
 */
public abstract class RedisZSetBuffer extends RedisBuffer {

    /**
     * 缓存的最终过期时间，用于服务宕机 Redis 自动删除缓存，默认为 6min
     */
    private static final int FINAL_CACHE_EXPIRE = 6;

    /**
     * 缓存的过期时间，用于服务定时删除缓存，默认为 3min
     */
    private static final int CACHE_EXPIRE = 1000 * 60 * 3;

    /**
     * 指定的 ZSet 缓存的键的 Set 集合
     */
    private final Set<String> keySet;

    /**
     * 清理过期数据时使用的锁
     */
    private final Lock cleanLock = new ReentrantLock();

    public RedisZSetBuffer(RedisTemplate<String, Object> redisTemplate) {
        super(redisTemplate);
        this.keySet = new HashSet<>();

        // 用于清理 ZSet 缓存的线程池，默认只有一个线程
        Executors.newScheduledThreadPool(1, r -> new Thread(r, "ZSet-cleaner")).scheduleAtFixedRate(
                this::clean, CACHE_EXPIRE, CACHE_EXPIRE, TimeUnit.MILLISECONDS
        );
    }

    // 清理过期数据
    private void clean() {
        // 复制 keySet 的数据，并清空它
        Set<String> keyToCleanSet;
        cleanLock.lock();
        try {
            keyToCleanSet = new HashSet<>(keySet);
            keySet.clear();
        } finally {
            cleanLock.unlock();
        }

        // 计算过期时间，清理过期数据
        long max = System.currentTimeMillis() - CACHE_EXPIRE;
        super.executePipelined(operations -> keyToCleanSet.forEach(
                key -> operations.opsForZSet().removeRangeByScore(key, 0, max)
        ));
    }

    /**
     * 从指定的数据集合中，过滤掉不在指定 ZSet 缓存中的数据
     *
     * @param key           指定 ZSet 的键
     * @param collection    待过滤的数据集合
     * @param missingFilter 缺失数据的过滤器，过滤出在数据库中的数据
     * @return 在缓存中的数据集合
     */
    protected Set<Long> filter(String key, Collection<Long> collection, Function<List<Long>, Set<Long>> missingFilter) {
        // 记录 key，用于删除
        keySet.add(key);

        // 过滤不在 ZSet 缓存中的数据
        Set<Long> existsSet = new HashSet<>();
        List<Long> missingList = new ArrayList<>();
        super.executePipelined(operations -> collection.forEach(ele -> {
            if (operations.opsForZSet().score(key, ele) == null) {
                missingList.add(ele);
            } else {
                existsSet.add(ele);
            }
        }));

        // 如果没有不存在的数据，则直接返回
        if (missingList.isEmpty()) {
            return existsSet;
        }

        // 如果数据库中也找不到缺失的数据，则直接返回
        Set<Long> foundSet = missingFilter.apply(missingList);
        if (foundSet.isEmpty()) {
            return existsSet;
        }

        // 缓存找到的数据
        double timestamp = currentTimestamp();
        Set<ZSetOperations.TypedTuple<Object>> tuples = new HashSet<>();
        foundSet.forEach(found -> tuples.add(new DefaultTypedTuple<>(found, timestamp)));
        redisTemplate.opsForZSet().add(key, tuples);
        redisTemplate.expire(key, FINAL_CACHE_EXPIRE, TimeUnit.MINUTES);

        // 将找到的数据加入存在数据的 set 中并返回
        existsSet.addAll(foundSet);
        return existsSet;
    }

    /**
     * 判断指定元素是否存在于指定 ZSet 的缓存中
     *
     * @param key            指定 ZSet 的键
     * @param setKey         指定缓存的键
     * @param missingCounter 当指定元素不存在时，在数据库中统计这个元素的个数，如果不为 0，则表示元素存在
     * @return 指定元素是否存在于指定 ZSet 的缓存中
     */
    protected boolean exists(String key, long setKey, IntSupplier missingCounter) {
        // 如果能查询到该元素的分数，则代表它存在
        if (redisTemplate.opsForZSet().score(key, setKey) != null) {
            return true;
        }

        // 在数据库中统计这个元素的个数，如果为 0，则表示元素不存在
        if (missingCounter.getAsInt() == 0) {
            return false;
        }

        // 将元素存储到 ZSet 中
        redisTemplate.opsForZSet().add(key, setKey, currentTimestamp());
        return true;
    }

    // 获取当前的时间戳
    private static long currentTimestamp() {
        return System.currentTimeMillis();
    }

    /**
     * 清理指定 ZSet 缓存下的指定缓存
     *
     * @param key    指定 ZSet 的键
     * @param setKey 指定缓存的键
     */
    protected void clean(String key, long setKey) {
        redisTemplate.opsForZSet().remove(key, setKey);
    }
}
